﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using Evaluant.Uss.ObjectContext;
using System.Collections;
using System.Reflection;

namespace Evaluant.Uss.Linq
{
    class EussQueryContext
    {
        internal void ExtractMethodCalls(List<MethodCallExpression> list, Expression expression)
        {
            if (expression.NodeType == ExpressionType.Call)
            {
                if (((MethodCallExpression)expression).Arguments.Count > 0)
                {
                    ExtractMethodCalls(list, ((MethodCallExpression)expression).Arguments[0]);
                }

                list.Add((MethodCallExpression)expression);
            }
        }

        internal Type GetRootType(Expression expression)
        {
            while (expression.NodeType == ExpressionType.Call)
            {
                expression = ((MethodCallExpression)expression).Arguments[0];
            }

            if (expression is ConstantExpression && IsIQueryable(((ConstantExpression)expression).Value.GetType()))
            {
                return GetGenericArgument(((ConstantExpression)expression).Value.GetType());
            }

            return null;
        }

        private bool IsIQueryable(Type type)
        {
            return typeof(IQueryable).IsAssignableFrom(type);
        }

        private Type GetGenericArgument(Type type)
        {
            foreach(Type iface in type.GetInterfaces())
            {
                if (iface.Name == "IQueryable`1")
                    return iface.GetGenericArguments()[0];
            }
            return typeof(object);
        }

        QueryTranslator translator;
        public Query Query { get; set; }
        public Evaluant.Uss.ObjectContext.ObjectContext ObjectContext { get; set; }
        Delegate selector;

        // Executes the expression tree that is passed to it.
        public object Execute<TResult>(Expression expression, ObjectContext.ObjectContext oc)
        {
            Type rootType = GetRootType(expression);
            Type elementType = TypeSystem.GetElementType(expression.Type);
            Type returnType = typeof(TResult).IsGenericType ? typeof(TResult).GetGenericArguments()[0] : typeof(TResult);
            this.Query = new Query(elementType);

            object linqQuery = Activator.CreateInstance(typeof(LinqQuery<,>).MakeGenericType(new Type[] { rootType, rootType }), rootType, oc);

            var i = new Evaluant.OPath.Expressions.Identifier(String.Empty, null);
            translator = new QueryTranslator(oc);
            translator.Path.Identifiers.Add(i);
            translator.Identifiers.Add(String.Empty, i);

            this.ObjectContext = oc;

            List<MethodCallExpression> expressions = new List<MethodCallExpression>();

            ExtractMethodCalls(expressions, expression);

            foreach (MethodCallExpression methodCall in expressions)
            {
                LambdaExpression lambda = null;
                MethodInfo methodInfo = null;
                Type[] genericArguments = linqQuery.GetType().GetGenericArguments();

                try
                {
                    switch (methodCall.Method.Name)
                    {
                        case "Where":
                            lambda = Expression.Lambda(((UnaryExpression)methodCall.Arguments[1]).Operand);

                            Type[] lambdaArguments = lambda.Body.Type.GetGenericArguments();
                            if (lambdaArguments.Length == 3)
                            {
                                throw new NotImplementedException("The indexed Where expression is not handled bu this provider. Please use ToList() before.");
                            }

                            methodInfo = typeof(LinqExtensions).GetMethod(methodCall.Method.Name).MakeGenericMethod(new Type[] { genericArguments[0] });
                            linqQuery = methodInfo.Invoke(null, new object[] { linqQuery, lambda.Body });
                            break;
                        case "Skip":
                        case "Take":
                            int value = (Int32)((ConstantExpression)methodCall.Arguments[1]).Value;
                            methodInfo = typeof(LinqExtensions).GetMethod(methodCall.Method.Name).MakeGenericMethod(new Type[] { genericArguments[0] });
                            linqQuery = methodInfo.Invoke(null, new object[] { linqQuery, value });
                            break;

                        case "OrderBy":
                        case "OrderByDescending":
                            lambda = Expression.Lambda(((UnaryExpression)methodCall.Arguments[1]).Operand);
                            methodInfo = typeof(LinqExtensions).GetMethod(methodCall.Method.Name).MakeGenericMethod(lambda.Body.Type.GetGenericArguments());
                            linqQuery = methodInfo.Invoke(null, new object[] { linqQuery, lambda.Body });
                            break;
                        case "First":
                        case "FirstOrDefault":
                        case "Count":
                            methodInfo = (from m in typeof(LinqExtensions).GetMethods()
                                          where m.Name == methodCall.Method.Name && m.GetParameters().Count() == methodCall.Method.GetParameters().Count()
                                          select m).First();

                            methodInfo = methodInfo.MakeGenericMethod(genericArguments);

                            if (methodInfo.GetParameters().Count() == 1)
                            {
                                linqQuery = methodInfo.Invoke(null, new object[] { linqQuery });
                            }
                            else
                            {
                                lambda = Expression.Lambda(((UnaryExpression)methodCall.Arguments[1]).Operand);
                                linqQuery = methodInfo.Invoke(null, new object[] { linqQuery, lambda.Body });
                            }
                            break;
                        case "Sum":
                        case "Min":
                        case "Max":
                        case "Average":
                            lambda = Expression.Lambda(((UnaryExpression)methodCall.Arguments[1]).Operand);
                            methodInfo = typeof(LinqExtensions).GetMethod(methodCall.Method.Name).MakeGenericMethod(new Type[] { genericArguments[0], genericArguments[1], lambda.Body.Type.GetGenericArguments()[1] });
                            linqQuery = methodInfo.Invoke(null, new object[] { linqQuery, lambda.Body });
                            break;
                        case "SelectMany":
                            MethodInfo mi = methodCall.Method;
                            methodInfo = (from m in typeof(LinqExtensions).GetMethods()
                                          where m.Name == methodCall.Method.Name && m.GetParameters().Count() == methodCall.Method.GetParameters().Count()
                                          select m).First();

                            methodInfo = methodInfo.MakeGenericMethod(mi.GetGenericArguments());

                            if (mi.GetGenericArguments().Count() == 2)
                            {
                                LambdaExpression lambda1 = Expression.Lambda(((UnaryExpression)methodCall.Arguments[1]).Operand);

                                linqQuery = methodInfo.Invoke(null, new object[] { linqQuery, lambda1.Body });
                            }
                            else
                            {
                                LambdaExpression lambda1 = Expression.Lambda(((UnaryExpression)methodCall.Arguments[1]).Operand);
                                LambdaExpression lambda2 = Expression.Lambda(((UnaryExpression)methodCall.Arguments[2]).Operand);

                                linqQuery = methodInfo.Invoke(null, new object[] { linqQuery, lambda1.Body, lambda2.Body });
                            }
                            break;
                        case "Select":
                            lambda = Expression.Lambda(((UnaryExpression)methodCall.Arguments[1]).Operand);
                            methodInfo = typeof(LinqExtensions).GetMethod(methodCall.Method.Name).MakeGenericMethod(lambda.Body.Type.GetGenericArguments());
                            linqQuery = methodInfo.Invoke(null, new object[] { linqQuery, lambda.Body });
                            break;
                    }
                }
                catch (TargetInvocationException e)
                {
                    // Intercept target invocation exception to rethrow the inner one
                    throw e.InnerException;
                }
            }
            
            return linqQuery;

            //return typeof(ObjectContext.ObjectContext).GetMethod("Load", new Type[] { typeof(Query) }).MakeGenericMethod(returnType).Invoke(oc, new object[] { Query });

            if (selector == null)
            {
                return typeof(ObjectContext.ObjectContext).GetMethod("Load", new Type[] { typeof(Query) }).MakeGenericMethod(returnType).Invoke(oc, new object[] { Query });
            }
            else
            {
                return null;
            }

        }

    }
}
